Go语言学习笔记 您所在的位置:网站首页 golang 高阶函数 Go语言学习笔记

Go语言学习笔记

2023-08-21 19:21| 来源: 网络整理| 查看: 265

视频来源:B站《golang入门到项目实战 [2022最新Go语言教程,没有废话,纯干货!]》

文章为自己整理的学习笔记,侵权即删,谢谢支持!

文章目录 一 golang 函数简介1.1 函数特性1.2 函数的定义和调用1.2.1 语法1.2.2 函数定义实例1.2.3 go语言函数调用 二 golang函数的返回值2.1 return的特性2.2 实例2.3 注意事项 三 golang函数的参数3.1 基本概念3.2 形参和实参3.3 函数参数传递方式3.4 参数按值传递3.5 变长参数 四 golang函数类型与函数变量4.1 基本概念4.2 应用实例 五 golang高阶函数5.1 函数作为参数5.2 函数作为返回值 六 匿名函数6.1 基本概念6.2 匿名函数的使用方式 七 golang闭包7.1 基础知识7.2 实例演示7.3 进阶实例7.4 闭包最佳应用场景 八 golang递归8.1 基础知识8.2 实例演示8.3 重要原则8.4 练习 九 golang defer语句9.1 基础知识9.2 为什么需要 defer9.3 实例演示9.4 注意事项9.5 实践应用场景 十 init函数10.1 基础知识10.2 实例演示

一 golang 函数简介

函数是go语言中的一级公民,我们把所有的功能单元都定义在函数中,可以重复使用。

函数包含函数的名称、参数列表和返回值类型,这些构成了函数的签名(signature)。

1.1 函数特性 go语言中有3种函数:普通函数、匿名函数(没有名称的函数)、方法(定义在struct上的函数)。go语言中不允许函数重载(overload),也就是说不允许函数同名。go语言中的函数不能嵌套函数,但可以嵌套匿名函数。函数是一个值,可以将函数赋值给变量,使得这个变量也成为函数。函数可以作为参数传递给另一个函数。函数的返回值可以是一个函数。函数调用的时候,如果有参数传递给函数,则先拷贝参数的副本,再将副本传递给函数。函数参数可以没有名称。 1.2 函数的定义和调用

函数在使用之前必须先定义,可以调用函数来完成某个任务。函数可以重复调用,从而达到代码重用。

1.2.1 语法 func function_name( [parameter list] ) [return_types] { 函数体 } func:函数由 func 开始声明function_name:函数名称,函数名和参数列表一起构成了函数签名。[parameter list]:参数列表,参数就像一个占位符,当函数被调用时,你可以将值传递给参数,这个值被称为实际参数。参数列表指定的是参数类型、顺序、及参数个数。参数是可选的,也就是说函数也可以不包含参数。return_types:返回类型,函数返回一列值。return_types 是该列值的数据类型。有些功能不需要返回值,这种情况下 return_types 不是必须的。函数体:函数定义的代码集合。 1.2.2 函数定义实例

① 定义一个求和函数

func sum(a int, b int) (ret int) { ret = a + b return ret }

② 定义一个比较两个数大小的函数

func compare(a int, b int) (max int) { if a > b { max = a } else { max = b } return max } 1.2.3 go语言函数调用

当我们要完成某个任务时,可以调用函数来完成。调用函数要传递参数,如何有返回值可以获得返回值。

例如:对1.2.2两例的函数进行调用

func main() { s := sum(1, 2) fmt.Printf("s: %v\n", s) max := compare(1, 2) fmt.Printf("max: %v\n", max) }

运行结果

s: 3 max: 2 二 golang函数的返回值

函数可以有0或多个返回值,返回值需要指定数据类型,返回值通过return关键字来指定。

2.1 return的特性 return可以有参数,也可以没有参数,这些返回值可以有名称,也可以没有名称。go中的函数可以有多个返回值。return关键字中指定了参数时,返回值可以不用名称。如果return省略参数,则返回值部分必须带名称当返回值有名称时,必须使用括号包围,逗号分隔,即使只有一个返回值但即使返回值命名了,return中也可以强制指定其它返回值的名称,也就是说return的优先级更高命名的返回值是预先声明好的,在函数内部可以直接使用,无需再次声明。命名返回值的名称不能和函数参数名称相同,否则报错提示变量重复定义return中可以有表达式,但不能出现赋值表达式,这和其它语言可能有所不同。例如return a+b是正确的,但return c=a+b是错误的。 2.2 实例

① 没有返回值

package main import "fmt" func f1() { fmt.Printf("我没有返回值") } func main() { f1() }

运行结果:

我没有返回值

② 有一个返回值

package main import "fmt" func sum(a int, b int) (ret int) { ret = a + b return ret } func main() { s := sum(1, 2) fmt.Printf("s: %v\n", s) }

运行结果:

s: 3

③ 有多个返回值,且在return中指定返回的内容

package main import "fmt" func f2() (name string, age int) { name = "Psych" age = 18 return name, age } func main() { name, age := f2() fmt.Printf("name: %v\n", name) fmt.Printf("age: %v\n", age) }

运行结果:

name: Psych age: 18

④ 多个返回值,返回值名称没有被使用

package main import "fmt" func f3() (name string, age int) { name = "Psych" age = 18 return // 等价于return name, age } func main() { name, age := f3() fmt.Printf("name: %v\n", name) fmt.Printf("age: %v\n", age) }

运行结果:

name: Psych age: 18

⑤ return覆盖命名返回值,返回值名称没有被使用

package main import "fmt" func f4() (name string, age int) { n := "Psych" // 重新声明 a := 18 return n, a } func main() { name, age := f4() fmt.Printf("name: %v\n", name) fmt.Printf("age: %v\n", age) }

运行结果:

name: Psych age: 18 2.3 注意事项

Go中经常会使用其中一个返回值作为函数是否执行成功、是否有错误信息的判断条件。例如return value,exists、return value,ok、return value,err等。

当函数的返回值过多时,例如有4个以上的返回值,应该将这些返回值收集到容器中,然后以返回容器的方式去返回。例如,同类型的返回值可以放进slice中,不同类型的返回值可以放进map中。

但函数有多个返回值时,如果其中某个或某几个返回值不想使用,可以通过下划线_来丢弃这些返回值。例如下面的f1函数两个返回值,调用该函数时,丢弃了第二个返回值b,只保留了第一个返回值a赋值给了变量a。

package main import "fmt" func f1() (int, int) { return 1, 2 } func main() { _, x := f1() fmt.Printf("x: %v\n", x) }

运行结果:

x: 2 三 golang函数的参数 3.1 基本概念 Go语言函数可以有0或多个参数,参数必须要指定数据类型。声明函数时的参数列表叫做形参,调用时传递的参数叫做实参。Go语言是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。 3.2 形参和实参

声明函数时的参数列表叫做形参,调用时传递的参数叫做实参。

func f1(a int, b int) int { // 其中a和b为形参 if a > b { return a } else { return b } } func main() { r := f1(1, 2) // 其中1和2为实参 fmt.Printf("r: %v\n", r) }

运行结果:

r: 2 3.3 函数参数传递方式

函数值类型参数默认就是值传递,而引用类型参数默认就是引用传递。

值类型:基本数据类型 int 系列, float 系列, bool, string 、数组和结构体 struct引用类型:指针、slice 切片、map、管道 chan、interface 等都是引用类型

不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。

值传递特点:变量直接存储值,内存通常在栈中分配引用传递特点:变量存储的是一个地址,这个地址对应的空间才真正存储数据,内存通常在堆上分配,当任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收如果希望函数内的变量能修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量。从效果上看类似引用 。 3.4 参数按值传递

Go语言函数是通过传值的方式传参的,意味着传递给函数的是拷贝后的副本,所以函数内部访问、修改的也是这个副本。

package main import "fmt" func f1(a int) { a = 200 fmt.Printf("a1: %v\n", a) } func main() { a := 100 f1(a) fmt.Printf("a: %v\n", a) }

运行结果:

a1: 200 a: 100

从运行结果可以看到,调用函数f1后,a的值并没有被改变,说明参数传递是拷贝了一个副本,也就是拷贝了一份新的内容进行运算。

map、slice、interface、channel这些数据类型本身就是指针类型的,所以就算是拷贝传值也是拷贝的指针,拷贝后的参数仍然指向底层数据结构,所以修改它们可能会影响外部数据结构的值。

package main import "fmt" func f1(a []int) { a[0] = 100 } func main() { a := []int{1, 2} f1(a) fmt.Printf("a: %v\n", a) }

运行结果:

a: [100 2]

从运行结果发现,调用函数后,slice内容被改变了。

3.5 变长参数

Go语言可以使用变长参数,有时候并不能确定参数的个数,可以使用变长参数,可以在函数定义语句的参数部分使用ARGS...TYPE的方式。这时会将...代表的参数全部保存到一个名为ARGS的slice中,注意这些参数的数据类型都是TYPE。

package main import "fmt" func f1(args ...int) { for _, v := range args { fmt.Printf("v: %v\n", v) } } func f2(name string, ok bool, args ...int) { fmt.Printf("name: %v\n", name) fmt.Printf("ok: %v\n", ok) for _, v := range args { fmt.Printf("v: %v\n", v) } } func main() { f1(1, 2, 3) fmt.Println("------------") f1(1, 2, 3, 4, 5, 6) fmt.Println("------------") f2("Psych", true, 1, 2, 3) }

运行结果:

v: 1 v: 2 v: 3 ------------ v: 1 v: 2 v: 3 v: 4 v: 5 v: 6 ------------ name: Psych ok: true v: 1 v: 2 v: 3 四 golang函数类型与函数变量 4.1 基本概念

可以使用type关键字来定义一个函数类型,语法格式如下:

type fun func(int, int) int

上面语句定义了一个fun函数类型,它也是一种数据类型

这种函数接收两个int类型的参数,并且返回一个int类型的返回值。

在 Go 中,函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用

4.2 应用实例

下面我们定义两个这样结构的两个函数,一个求和,一个比较大小:

func sum(a int, b int) int { return a + b } func max(a int, b int) int { if a > b { return a } else { return b } }

下面定义一个fun函数类型,把sum和max赋值给它

package main import "fmt" type fun func(int, int) int func sum(a int, b int) int { return a + b } func max(a int, b int) int { if a > b { return a } else { return b } } func main() { var f fun f = sum fmt.Printf("f的数据类型是:%T\nsum的数据类型是:%T\n", f, sum) s := f(1, 2) fmt.Printf("s: %v\n", s) f = max m := f(3, 4) fmt.Printf("m: %v\n", m) }

运行结果:

f的数据类型是:main.fun sum的数据类型是:func(int, int) int s: 3 m: 4

由此我们可以看出函数也是一种数据类型,可以赋值给一个变量,则该变量就是一个函数类型的变量了。通过该变量可以对函数调用。

因此函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用

package main import "fmt" func sum(a int, b int) int { return a + b } func myFun(funvar func(int, int) int, num1 int, num2 int) int { return funvar(num1, num2) } func main() { res := myFun(sum, 20, 30) fmt.Printf("res: %v\n", res) }

运行结果:

res: 50 五 golang高阶函数

由4.2了解到了函数既然是一种数据类型,因此在 Go 中,函数可以作为形参,并且调用。

其实go语言的函数,可以作为函数的参数,传递给另外一个函数,也可作为另外一个函数的返回值返回。

5.1 函数作为参数 package main import "fmt" func sayHello(name string) { fmt.Printf("Hello,%s", name) } func f1(name string, f func(string)) { f(name) } func main() { f1("golang", sayHello) }

运行结果:

Hello,golang 5.2 函数作为返回值 package main import "fmt" func add(x, y int) int { return x + y } func sub(x, y int) int { return x - y } func cal(s string) func(int, int) int { switch s { case "+": return add case "-": return sub default: return nil } } func main() { add := cal("+") r := add(1, 2) fmt.Printf("r: %v\n", r) fmt.Println("-----------") sub := cal("-") r = sub(100, 50) fmt.Printf("r: %v\n", r) }

运行结果:

r: 3 ----------- r: 50 六 匿名函数 6.1 基本概念

go语言函数不能嵌套,但是在函数内部可以定义匿名函数,实现一下简单功能调用。

所谓匿名函数就是,没有名称的函数。

语法格式如下:

func (参数列表)(返回值)

当然可以既没有参数,可以没有返回值,

匿名函数也可以实现多次调用。

6.2 匿名函数的使用方式

① 方式一:在定义匿名函数时就直接调用

这种方式匿名函数只能调用一次

package main import "fmt" func main() { func(a int, b int) { max := 0 if a > b { max = a } else { max = b } fmt.Printf("max: %v\n", max) }(1, 2) // 直接调用,自动执行 }

运行结果:

max: 2

② 方式二:将匿名函数赋给一个变量(函数变量),再通过该变量来调用匿名函数

package main import "fmt" func main() { max := func(a int, b int) int { if a > b { return a } else { return b } } i := max(1, 2) fmt.Printf("i: %v\n", i) }

运行结果:

i: 2

③ 方式三:全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数,可以在程序有效。

package main import "fmt" var Fun1 = func(n1 int, n2 int) int { // 此时Fun1是一个全局匿名函数 return n1 * n2 } func main() { res := Fun1(4, 9) // 全局匿名函数的使用 fmt.Printf("res: %v\n", res) }

运行结果:

res: 36 七 golang闭包 7.1 基础知识

闭包可以理解成定义在一个函数内部的函数。

在本质上,闭包是将函数内部和函数外部连接起来的桥梁,或者说是函数和其引用环境的组合体。

闭包指的是一个函数和与其相关的引用环境组合而成的实体。

简单来说,闭包=函数+引用环境。

7.2 实例演示 package main import "fmt" func add() func(int) int { var n int = 10 return func(x int) int { n = n + x return n } } func main() { var f = add() fmt.Println(f(10)) fmt.Println(f(20)) fmt.Println(f(30)) fmt.Println("-----------") f1 := add() fmt.Println(f1(40)) fmt.Println(f1(50)) }

代码解释说明:

add()是一个函数,返回的数据类型是fun(int)int

闭包说明:

var n int = 10 return func(x int) int { n = n + x return n }

返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个匿名函数就和n形成一个整体,构成闭包

可以这样理解:闭包是类, 函数是操作,n 是字段。函数和它使用到 n 构成闭包。

当我们反复的调用 f 或者 f1 函数时,因为 n 是初始化一次,因此每调用一次就进行累计。

要搞清楚闭包的关键,就是要分析出返回的函数使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包。

在 f 的生命周期内,变量 n 一直有效。

7.3 进阶实例 package main import ( "fmt" ) func calc(base int) (func(int) int, func(int) int) { add := func(i int) int { base += i return base } sub := func(i int) int { base -= i return base } return add, sub } func main() { f1, f2 := calc(10) fmt.Println(f1(1), f2(2)) fmt.Println(f1(3), f2(4)) fmt.Println(f1(5), f2(6)) }

运行结果:

11 9 12 8 13 7 7.4 闭包最佳应用场景

请编写一个程序,具体要求如下

编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg) ,则返回 文件名.jpg , 如果已经有.jpg 后缀,则返回原文件名。要求使用闭包的方式完成strings.HasSuffix , 该函数可以判断某个字符串是否有指定的后缀。 package main import ( "fmt" "strings" ) func makeSuffixFunc(suffix string) func(string) string { return func(name string) string { // 若 name 没有指定后缀则加上,否则返回原来的名字 if !strings.HasSuffix(name, suffix) { return name + suffix } return name } } func main() { f := makeSuffixFunc(".jpg") fmt.Printf("文件名处理后: %v\n", f("winter")) fmt.Printf("文件名处理后: %v\n", f("bird.jpg")) }

代码解释说明:

返回的匿名函数和 makeSuffix (suffix string) 的 suffix 变量 组合成一个闭包,因为 返回的函数引用到 suffix 这个变量我们体会一下闭包的好处:如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入 后缀名,比如 .jpg ,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用。 八 golang递归 8.1 基础知识

函数内部调用函数自身的函数称为递归函数

使用递归函数最重要的三点:

递归就是自己调用自己。必须先定义函数的退出条件,没有退出条件,递归将成为死循环。go语言递归函数很可能会产生一大堆的goroutine,也很可能会出现栈空间内存溢出问题。 8.2 实例演示

① 阶乘

package main import "fmt" func a(n int) int { // 返回条件 if n == 1 { return 1 } else { // 自己调用自己 return n * a(n-1) } } func main() { n := 5 // 5! = 5x4x3x2x1 r := a(n) fmt.Printf("r: %v\n", r) }

运行结果:

r: 120

② 斐波那契数列

斐波拉契数列的计算公式为f(n)=f(n-1)+f(n-2)且f(2)=f(1)=1

package main import "fmt" func f(n int) int { // 退出点判断 if n == 1 || n == 2 { return 1 } // 递归表达式 return f(n-1) + f(n-2) } func main() { r := f(5) fmt.Printf("r: %v\n", r) }

运行结果:

r: 5 8.3 重要原则 执行一个递归时,就创建一个新的受保护的独立空间(新函数栈)递归的局部变量是独立的,不会相互影响递归必须向退出递归的条件逼近,否则就是无限递归(死循环)当一个递归执行完毕,或者遇到 return,就会返回,遵守谁调用,就将结果返回给谁,同时当函数执行完毕或者返回时,该递归本身也会被系统销毁 8.4 练习

有一堆桃子,猴子第一天吃了其中的一半,并再多吃了一个!以后每天猴子都吃其中的一半,然后再多吃一个。当到第十天时,想再吃时(还没吃),发现只有 1 个桃子了。问题:最初共多少个桃子?

思路分析:

第 10 天只有一个桃子第 9 天有几个桃子 = (第 10 天桃子数量 + 1) * 2规律: 第 n 天的桃子数据 peach(n) = (peach(n+1) + 1) * 2 package main import ( "fmt" ) func peach(n int) int { if n > 10 || n return 1 } else { return (peach(n+1) + 1) * 2 } } func main() { a := peach(1) fmt.Printf("第一天最初的桃子数量是: %v\n", a) }

运行结果:

第一天最初的桃子数量是: 1534 九 golang defer语句 9.1 基础知识

go语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

defer的特性:

关键字 defer 用于注册延迟调用。这些调用直到 return 前才被执。因此,可以用来做资源清理。多个defer语句,按先进后出的方式执行。defer语句中的变量,在defer声明时就决定了。遵守栈stack,后进先出。 9.2 为什么需要 defer 关闭文件句柄锁资源释放数据库连接释放 9.3 实例演示

例如查看执行顺序

package main import "fmt" func main() { fmt.Println("start") defer fmt.Println("step1") defer fmt.Println("step2") defer fmt.Println("step3") fmt.Println("end") }

运行结果:

start end step3 step2 step1 9.4 注意事项

当 go 执行到一个 defer 时,不会立即执行 defer 后的语句,而是将 defer 后的语句压入到一个栈中[暂时称该栈为 defer 栈], 然后继续执行函数下一个语句。

当函数执行完毕后,在从 defer 栈中,依次从栈顶取出语句执行(注:遵守栈 先入后出的机制)

在 defer 将语句放入到栈时,也会将相关的值拷贝同时入栈。请看一段代码:

package main import ( "fmt" ) func sum(n1 int, n2 int) int { //当执行到defer时,暂时不执行,会将defer后面的语句先压入到一个独立的栈 //当函数执行完毕时,再从defer的栈按照先入后出的原则出栈并执行 defer fmt.Println("ok1 n1 = ", n1) // n1=10 defer fmt.Println("ok2 n2 = ", n2) // n2=20 n1++ //n1=11 n2++ //n2=21 res := n1 + n2 //res=32 fmt.Printf("ok3 res= %v\n", res) return res } func main() { res := sum(10, 20) fmt.Printf("res= %v\n", res) //res=32 }

运行结果:

ok3 res= 32 ok2 n2 = 20 ok1 n1 = 10 res= 32 9.5 实践应用场景

defer 最主要的价值是在,当函数执行完毕后,可以及时的释放函数创建的资源。

看下模拟代码:

func test(){ //关闭文件资源 file = openfile(文件名) defer file.close() // 其他代码 } func test(){ //释放数据库资源 connect = openDatabse() defer connect.close() // 其他代码 }

说明:

在 golang 编程中的通常做法是,创建资源后,比如(打开了文件,获取了数据库的链接,或者是锁资源), 可以执行 defer file.Close()或者 defer connect.Close()在 defer 后,可以继续使用创建资源.当函数完毕后,系统会依次从 defer 栈中,取出语句,关闭资源.这种机制,非常简洁,程序员不用再为在什么时机关闭资源而烦心。 十 init函数 10.1 基础知识

golang有一个特殊的函数init函数,先于main函数执行,实现包级别的一些初始化操作。

主要特点:

init函数先于main函数自动执行,不能被其他函数调用init函数没有输入参数、返回值每个包可以有多个init函数包的每个源文件也可以有多个init函数,这点比较特殊同一个包的init执行顺序,golang没有明确定义,编程时要注意程序不要依赖这个执行顺序不同包的init函数按照包导入的依赖关系决定执行顺序golang代码初始化顺序:变量初始化->init()->main() 10.2 实例演示 package main import "fmt" var i int = initVar() func init() { fmt.Println("init2") } func init() { fmt.Println("init...") } func initVar() int { fmt.Println("initVar...") return 100 } func main() { fmt.Println("main....") }

运行结果:

initVar... init2 init... main....


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有